//-----------------------------------------------------------------------
// <copyright file="FxCop.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
// </copyright>
// <author>Andy Reeves</author>
// <email>andyr</email>
// <date>2004-03-29</date>
// <summary>Runs FxCop analysis on the assemblies specified.</summary>
//-----------------------------------------------------------------------

namespace Microsoft.Sdc.Tasks.Tools
{
    using System;
    using System.Xml;
    using System.Globalization;
    using System.IO;
    using System.Text.RegularExpressions;
    using Microsoft.Win32;
    using Microsoft.Build.Framework;
    using Microsoft.Build.Tasks;
    using Microsoft.Sdc.Tasks.Configuration;
    using Microsoft.Build.Utilities;


    using System.Diagnostics;

    /// <summary>
    /// Runs FxCop analysis on the assemblies specified, creates a summary file and returns the total number of violations.
    /// </summary>          
    /// <remarks>
    /// <code><![CDATA[
    /// <Tools.FxCop Assemblies="assemblies"
    ///              ProjectFilePath="projectFilePath"
    ///              ProjectTemplateFilePath="projectTemplateFilePath"
    ///              OutFileName="outFileName">
    ///     <Output TaskParameter="TotalViolations" ItemName="itemName" />
    /// </Tools.FxCop>
    /// ]]></code>
    /// <para>where:</para>
    /// <para><i>assemblies (Required)</i></para>
    /// <para>The fullpaths to the assemblies to analyze.</para>
    /// <para><i>projectFilePath (Required)</i></para>
    /// <para>This will be the FxCop file that will be created from the template.</para>
    /// <para><i>projectTemplateFilePath (Required)</i></para>
    /// <para>An .FxCop file to use as a template.</para>
    /// <para><i>outFileName (Required)</i></para>
    /// <para>The name of the file to create with the FxCop summary in it.</para>
    /// <para><i>itemName</i></para>
    /// <para>The name of the item to create for MSBuild. It will be referred to as @(itemName).</para>
    /// </remarks>
    /// <example>
    /// <code><![CDATA[
    /// <Project>
    ///     <ItemGroup>
    ///         <Assemblies Include="Microsoft.Sdc.Tasks.Configuration.Tasks.dll" />
    ///         <Assemblies Include="Microsoft.Sdc.Tasks.Configuration.Tasks.Test.dll" />
    ///     </ItemGroup>
    ///     <Target Name="Test" >
    ///         <Tools.FxCop
    ///             ContinueOnError="true"
    ///             Assemblies="@(Assemblies->'%(FullPath)')" 
    ///             ProjectFilePath="$(MSBuildProjectDirectory)\Output\Main\Main.FxCop" 
    ///             ProjectTemplateFilePath="$(MSBuildProjectDirectory)\FxCop\Main\main.FxCop" 
    ///             OutFileName="$(MSBuildProjectDirectory)\Output\Main\FxCopErrors.xml">
    ///             <Output TaskParameter="TotalViolations" ItemName="FxCopViolations" />
    ///         </Tools.FxCop> 
    ///         <Message Text="@(FxCopViolations) FxCop violations" />       
    ///     </Target>
    /// </Project>
    /// ]]></code>    
    /// </example>
    public class FxCop : ToolTaskBase
    {        
        /// <summary>
        /// Initializes a new instance of the FxCop class.
        /// </summary>
        public FxCop()
        {
        }

        /// <summary>
        /// This will be the FxCop file that will be created from the template.
        /// </summary>
        /// <value></value>
        [Required]
        public string ProjectFilePath
        {
            get { return (string) Bag["ProjectFilePath"]; }
            set { Bag["ProjectFilePath"] = value; }
        }

        /// <summary>
        /// An .FxCop file to use as a template.
        /// </summary>
        /// <value></value>
        [Required]
        public string ProjectTemplateFilePath
        {
            get { return (string) Bag["ProjectTemplateFilePath"]; }
            set { Bag["ProjectTemplateFilePath"] = value; }
        }

        public string FxCopNotifyExe
        {
            get { return (string) Bag["FxCopNotifyExe"]; }
            set { Bag["FxCopNotifyExe"] = value; }
        }

        public string SmtpServer
        {
            get { return (string) Bag["SmtpServer"]; }
            set { Bag["SmtpServer"] = value; }
        }

        public string OwnersXmlFile
        {
            get { return (string) Bag["OwnersXmlFile"]; }
            set { Bag["OwnersXmlFile"] = value; }
        }

        public string XslFile
        {
            get { return (string) Bag["XslFile"]; }
            set { Bag["XslFile"] = value; }
        }

        public string XslReferenceLocation
        {
            get { return (string) Bag["XslReferenceLocation"]; }
            set { Bag["XslReferenceLocation"] = value; }
        }

        /// <summary>
        /// The name of the file to create with the FxCop summary in it.
        /// </summary>
        /// <value></value>
        [Required]
        public string OutFileName
        {
            get { return (string) Bag["OutFileName"]; }
            set { Bag["OutFileName"] = value; }
        }

        /// <summary>
        /// he fullpaths to the assemblies to analyze.
        /// </summary>
        /// <value></value>
        [Required]
        public string[] Assemblies
        {
            get { return ((string[])Bag["Assemblies"]); }
            set { Bag["Assemblies"] = value; }
        }      

        /// <summary>
        /// The number of FxCop violations found
        /// </summary>
        /// <value></value>
        [Output]
        public int TotalViolations
        {
            get { return this.GetIntParameterWithDefault("TotalViolations", 0); }
        }

        /// <summary>
        /// The name of the exe we call.
        /// </summary>
        /// <value>The name of the exe we call.</value>
        protected override string ToolName
        {
            get { return "FxCopCmd.exe"; }
        }

        /// <summary>
        /// Full calculated path to tool including exe name
        /// </summary>
        /// <returns>Defaults to default install location of FxCop</returns>
        protected override string GenerateFullPathToTool()
        {
            if (this.ProjectTemplateFilePath == null || this.ProjectTemplateFilePath.Length == 0)
            {
                throw new ApplicationException("Cannot calculate path to FxCop unless a template .FxCop file is provided.");
            }
            string version = this.GetVersion(this.ProjectTemplateFilePath);

            return System.IO.Path.Combine(this.GetPathToFxCop(version), this.ToolName);
        }        

        /// <summary>
        /// Builds the commandline for this tool
        /// </summary>
        /// <param name="commandLine">The object to append the args to.</param>
        /// <returns>True if successfully added args.</returns>
        protected override void AddCommandLineCommands(CommandLineBuilderExtension commandLine)
        {
            AppendSwitchAndValueWhenNotStringEmpty(commandLine, "/project:", "ProjectFilePath", String.Empty);
            AppendSwitchAndValueWhenNotStringEmpty(commandLine, "/out:", "OutFileName", String.Empty);
        }

        /// <summary>
        /// Performs the action of this task.
        /// </summary>
        protected override void InternalExecute()
        {
            #region Execute code

            // Retry Fxcop if it exits with an error. We have had issues where the Fxcop process occasionaly 
            // appears to see the generated project file as empty (perhaps due to NTFS write cache?)
            for (int retry = 1; retry <= 3; retry++)
            {
                this.SetUpFxCopProject(this.Assemblies, this.ProjectTemplateFilePath, this.ProjectFilePath);

                Bag["TotalViolations"] = 0;

                base.InternalExecute();
                if (this.ExitCode == 0 || this.ExitCode == 4)
                {
                    break;
                }
                else
                {
                    Console.WriteLine("Warning: attempt {0} to run FxCop FAILED with exit code {1}.", retry, this.ExitCode);
                    System.Threading.Thread.Sleep(1000);
                }
#if !DEBUG
                File.Delete(this.ProjectFilePath);
#endif
            }

            if (this.ExitCode != 0 && this.ExitCode != 4)
            {

                Console.WriteLine("ERROR: FxCop Tool retries exhausted");
                // setting violations to a large number. Unfortunately most scripts call Fxcop task with
                // StopOnFailure set to false, hence throwing an exception here would not stop the build
                Bag["TotalViolations"] = 999;
            }

            if (System.IO.File.Exists(this.OutFileName))
            {
                //a violation has been logged or it doesn't create this file
                //Now add the XSL reference into the XML results - so that everyone can view it with the transformation.
                string xslReference = String.Format(CultureInfo.InvariantCulture, "type=\"text/xsl\" href=\"{0}\"", this.XslReferenceLocation);

                XmlDocument failures = new XmlDocument();
                failures.Load(this.OutFileName);

                XmlProcessingInstruction newPI;
                newPI = failures.CreateProcessingInstruction("xml-stylesheet", xslReference);

                // Add the processing instruction node to the document.
                failures.InsertBefore(newPI, failures.DocumentElement);
                failures.Save(this.OutFileName);
                if (this.SmtpServer != null && this.SmtpServer.Length > 0)
                {
                    this.MailFailuresToOwners(this.OutFileName, this.SmtpServer, this.OwnersXmlFile, this.XslFile, this.FxCopNotifyExe);
                }

                XmlNodeList failuresList = failures.SelectNodes("//Message[@Status='Active']");

                Bag["TotalViolations"] = failuresList.Count;
            }
            #endregion
        }

        private string GetVersion(string projectPath)
        {
            XmlDocument masterXml = new XmlDocument();
            masterXml.Load(projectPath);
            XmlNode targetNode = masterXml.SelectSingleNode("FxCopProject");
            string version = targetNode.Attributes["Version"].Value;
            double v = Convert.ToDouble(version);

            return v.ToString("0.00");
        }

        private void SetUpFxCopProject(string[] assemblies, string projectTemplateFilePath, string projectFilePath)
        {
            string version = this.GetVersion(projectTemplateFilePath);
            switch (version)
            {
                case "1.09":
                    this.SetUpFxCopProject109(assemblies, projectTemplateFilePath, projectFilePath);
                    break;

                default:
                    this.SetUpFxCopProject122(assemblies, projectTemplateFilePath, projectFilePath);
                    break;
            }
        }

        private void SetUpFxCopProject109(string[] assemblies, string projectTemplateFilePath, string projectFilePath)
        {
            //What if assemblies has no elements?
            if (assemblies.Length == 0)
            {
                Log.LogError("FxCop.NoAssemblies");
                return;
            }

            //Load .fxcop file into a DOM - use it as a template
            //We will add the details of all of the assemblies we want to analyse
            XmlDocument masterXml = new XmlDocument();
            masterXml.Load(projectTemplateFilePath);

            //Remove all <TargetAssembly> elements (if any)
            //Then loop through the assembly list, adding each one as a <TargetAssembly> element

            XmlNode targetNode = masterXml.SelectSingleNode("FxCopProject/TargetAssemblies");
            if (targetNode == null)
            {
                Log.LogError("FxCop.NoTargetAssemblies");
                return;
            }
            targetNode.RemoveAll();

            foreach (string assembly in assemblies)
            {
                XmlElement newElem = masterXml.CreateElement("TargetAssembly");
                newElem.InnerText = assembly;

                XmlAttribute newAttr = masterXml.CreateAttribute("Visible");
                newAttr.Value = "True";

                newElem.SetAttributeNode(newAttr);
                targetNode.AppendChild(newElem);
            }

            //Save the document to the Results file location specified in the .config file
            masterXml.Save(projectFilePath);
        }

        private void SetUpFxCopProject122(string[] assemblies, string projectTemplateFilePath, string projectFilePath)
        {
            //What if assemblies has no elements?
            if (assemblies.Length == 0)
            {
                Log.LogError("FxCop.NoAssemblies");
                return;
            }

            Debug.WriteLine("About to load the template file into a DOM");
            //Load .fxcop file into a DOM - use it as a template
            //We will add the details of all of the assemblies we want to analyse
            XmlDocument masterXml = new XmlDocument();
            masterXml.Load(projectTemplateFilePath);

            //Remove all <TargetAssembly> elements (if any)
            //Then loop through the assembly list, adding each one as a <TargetAssembly> element

            XmlNode targetNode = masterXml.SelectSingleNode("FxCopProject/Targets");
            XmlNode referencesNode = masterXml.SelectSingleNode("FxCopProject/Targets/AssemblyReferenceDirectories");
            
            if (targetNode == null)
            {
                Log.LogError("FxCop.NoTargetAssemblies");
                return;
            }
            targetNode.RemoveAll();
            
            // Restore the references node (if there was one)
            if (referencesNode != null)
            {
                targetNode.AppendChild(referencesNode.Clone());
            }

            Debug.WriteLine("Adding nodes for each assembly");

            //Code modified to reflect changes in FxCop 1.20 and above as template the struct has changed.
            foreach (string assembly in assemblies)
            {
                XmlElement newElem = masterXml.CreateElement("Target");

                XmlAttribute newAttr = masterXml.CreateAttribute("Name");
                newAttr.Value = assembly.ToString();

                XmlAttribute newAttr1 = masterXml.CreateAttribute("Analyze");

                newAttr1.Value = "True";

                XmlAttribute newAttr2 = masterXml.CreateAttribute("AnalyzeAllChildren");
                newAttr2.Value = "True";

                newElem.SetAttributeNode(newAttr);
                targetNode.AppendChild(newElem);

                newElem.SetAttributeNode(newAttr1);
                targetNode.AppendChild(newElem);

                newElem.SetAttributeNode(newAttr2);
                targetNode.AppendChild(newElem);
            }
            Debug.WriteLine("Saving to the projectfilepath");

            //Save the document to the Results file location specified
            masterXml.Save(projectFilePath);
        }

        private void MailFailuresToOwners(string fxcopFailuresFile, string smtpServer, string ownersXmlFile, string xslFile, string fxcopNotifyExePath)
        {
            this.Log.LogMessageFromResources("FxCopMailingFailures");
            string args = String.Format(CultureInfo.InvariantCulture, "/m:{0} /o:{1} /r:{2} /s:{3} /x:{3}", smtpServer, ownersXmlFile, fxcopFailuresFile, xslFile);

            //e.g.  /m:smarthost /o:fxowners.xml /r:fxreport.xml /s:ViolationsReportDhtml.xsl /x:ViolationsReportDhtml.xsl
            ShellExecute shellExecute = new ShellExecute(fxcopNotifyExePath, args);
            shellExecute.Execute();
            string error = shellExecute.StandardError;
            Log.LogMessage(error);
        }

        private string GetPathToFxCop(string version)
        {
            switch (version)
            {
                case "1.09":
                    try
                    {
                        //@"C:\Program Files\Microsoft FxCop";
                        string s;

                        RegistryKey fxcopKey = Registry.ClassesRoot.OpenSubKey(@"Applications\FxCop.exe\shell\Open\Command");
                        string registryPathToFxCop = Convert.ToString(fxcopKey.GetValue(""), CultureInfo.InvariantCulture);
                        fxcopKey.Close();
                        Regex regex = new Regex(@"(.+)FxCop\.exe.+", RegexOptions.IgnoreCase);
                        Match m = regex.Match(registryPathToFxCop);
                        s = m.Groups[1].Value;
                        s = s.Replace("\"", "");

                        Log.LogMessageFromResources(MessageImportance.Low, "FxCop.Path", s);
                        return s;
                    }
                    catch
                    {
                        Log.LogMessageFromResources("FxCop.RegistryKeyDefaulting");
                        return System.Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\Microsoft FxCop");
                    }

                case "1.22":
                default:

                    try
                    {
                        //@"C:\Program Files\Microsoft FxCop 1.22";
                        RegistryKey fxcopKey = null;
                        string pathToFxCop;
                        try
                        {
                            fxcopKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\FxCop");
                            if (fxcopKey == null)
                            {
                                fxcopKey = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\FxCop");
                            }
                            pathToFxCop = Convert.ToString(fxcopKey.GetValue("InstallRoot"), CultureInfo.InvariantCulture);
                        }
                        finally
                        {
                            fxcopKey.Close();
                        }
                        Log.LogMessageFromResources(MessageImportance.Low, "FxCop.Path", pathToFxCop);
                        return pathToFxCop;
                    }
                    catch
                    {
                        Log.LogMessageFromResources(MessageImportance.Low, "FxCop.RegistryKeyDefaulting");
                        return System.Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\Microsoft FxCop " + version);
                    }
            }
        }
    }
}